/**
 * This class parse OXM XML to a more "application-specific" format
 * Vertices are created here
 */


define(
[
"framework/math/Polygon", "framework/asset/AssetSyst", 
"util/BulkLoader",
"framework/math/GLMatrix",
"util/GeoMath"
],
function(Polygon, AssetSyst, BulkLoader){

    //Settings
    var _INDOOR_HEIGHT_MUL = 1/5;


    //Helper
    function _getID(xml) {
        return $(xml).attr("id");
    }




    ////Low level function for creating visuals

    function _processNode(xml, convF, out) {
        out.push(convF.latToMeter(+$(xml).attr("lat"))); //x
        out.push(convF.lonToMeter(+$(xml).attr("lon"))); //z
    }

    function _processWayVisual(src, xml, yOffset, useRoof, useFloor, convF, height, wallInward, e) {
        //Temp storage for vertex data
        var temp    = [];
        var pos     = [];
        var indices = [];
        var lineIndices = [];

        //Get base vertices
        var vertices = $(xml).find('nd');
        var i = 0;
        for(i = 0; i < vertices.length - 1; ++i) {
            var vertex = vertices[i];
            var id = $(vertex).attr("ref");
            _processNode($(src).find("#"+id)[0], convF, temp);
        }

        //Triangulate base vertices, it' important to be done here since
        //Polygon class also fix the winding order of polygon, forcing it to
        //be CCW. This winding order is important to build the wall indices
        var p = Polygon.create();
        p.addVertices(temp);
        var baseIndices = p.triangulate();
        temp = p.getVertices();

        //Get height
        if(!height) {
            //Get from tag
            height = $(xml).find('tag[k="height"]')[0];
            height = height || $(xml).find('tag[k="building:height"]')[0];
            height = +$(height).attr('v');

            //Infer from level
            height = height || $(xml).find('tag[k="building:levels"]')[0];
            height = height || $(xml).find('tag[k="level"]')[0];
            height = height ? (+$(height).attr('v') * 6) : false;

            //Fallback - auto assign 3 with randomization
            height = !height ? (6 + Math.random() * 4) : height;
        }

        //Build walls
        n 	= temp.length;
        if(wallInward) {
            for(i = 0; i < n; ++i) {
                //Push indices
                var iOffset = pos.length/3;
                indices.push(
                    iOffset + 0, iOffset + 1, iOffset + 2,
                    iOffset + 1, iOffset + 3, iOffset + 2
                );
                lineIndices.push(
                    iOffset + 0, iOffset + 1,
                    iOffset + 1, iOffset + 3,
                    iOffset + 3, iOffset + 2,
                    iOffset + 2, iOffset + 0
                );

                //Push positions
                pos.push(
                    temp[i][0], yOffset, temp[i][1], //BL
                    temp[(i+1)%n][0], yOffset, temp[(i+1)%n][1], //BR
                    temp[i][0], yOffset + height, temp[i][1], //TL
                    temp[(i+1)%n][0], yOffset + height, temp[(i+1)%n][1] //TR
                );
            }
        } else {
            for(i = 0; i < n; ++i) {
                //Push indices
                var iOffset = pos.length/3;
                indices.push(
                    iOffset + 2, iOffset + 1, iOffset + 0,
                    iOffset + 2, iOffset + 3, iOffset + 1
                );
                lineIndices.push(
                    iOffset + 0, iOffset + 1,
                    iOffset + 1, iOffset + 3,
                    iOffset + 3, iOffset + 2,
                    iOffset + 2, iOffset + 0
                );

                //Push positions
                pos.push(
                    temp[i][0], yOffset, temp[i][1], //BL
                    temp[(i+1)%n][0], yOffset, temp[(i+1)%n][1], //BR
                    temp[i][0], yOffset + height, temp[i][1], //TL
                    temp[(i+1)%n][0], yOffset + height, temp[(i+1)%n][1] //TR
                );
            }
        }

        //Build roof & floor indices
        var idx = baseIndices;
        if(idx != null) {
            if(useRoof) {
                for(i = 0; i < idx.length; ++i) {
                    //Push indices
                    indices.push(pos.length/3);

                    //Push position
                    pos.push(
                        temp[idx[i]][0], yOffset + height, temp[idx[i]][1]
                    )
                }
            }

            if(useFloor) {
                for(i = 0; i < idx.length; ++i) {
                    //Push indices
                    indices.push(pos.length/3);

                    //Push position
                    pos.push(
                        temp[idx[i]][0], yOffset, temp[idx[i]][1]
                    )
                }
            }
        }

        //Build normal for each vertex
        var normals = [];
        function _computeNormal(idx1, idx2, idx3) {
            var i1 = indices[idx1];
            var i2 = indices[idx2];
            var i3 = indices[idx3]

            var v1 = vec3.create([pos[i1 * 3], pos[i1 * 3 + 1], pos[i1 * 3 + 2]]);
            var v2 = vec3.create([pos[i2 * 3], pos[i2 * 3 + 1], pos[i2 * 3 + 2]]);
            var v3 = vec3.create([pos[i3 * 3], pos[i3 * 3 + 1], pos[i3 * 3 + 2]]);

            v1 = vec3.subtract(v1, v2, v1);
            v3 = vec3.subtract(v3, v2, v3);
            v2 = vec3.cross(v3, v1, v2);
            v2 = vec3.normalize(v2, v2);

            //add to current
            if(!normals[i2]) normals[i2] = vec3.create();
            normals[i2] = vec3.add(v2, normals[i2], normals[i2])

            //normalize again
            normals[i2] = vec3.normalize(normals[i2], normals[i2]);
        }
        for(var i = 0; i < indices.length; i += 3) {
            _computeNormal(i, i+1, i+2);
            _computeNormal(i+2, i, i+1);
            _computeNormal(i+1, i+2, i);
        }

        //flatten normals
        var flattenedNormals = [];
        normals.forEach(function(val){
            flattenedNormals.push(val[0], val[1], val[2]);
        });

        //Put the values to entity
        e.visual = {
            pos: pos,
            indices: indices,
            lineIndices: lineIndices,
            normals: flattenedNormals
        }
    }

    function _processBuildingVisualInWay(src, xml, convF, bulkLoader, yOffset, height, useRoof, useFloor, wallInward, e) {
        var model3D = _getModel3D(xml);
        if(model3D) 
            _loadModel3D(src, $(src).find("#"+model3D)[0], convF, bulkLoader, yOffset, e);
        else  
            _processWayVisual(src, xml, yOffset, useRoof, useFloor, convF, height, wallInward, e);
    }

    function _processRelationVisual(src, xml, processed, convF, bulkLoader, e) {
        //Get ref id
        var member = $(xml).find('member[role="level_0"]')[0];
        var id = $(member).attr("ref");

        //Get member's id which role="shell"
        member = $(src).find('#' + id)[0];
        member = $(member).find('member[role="shell"]');
        if(!member) return null; //Exit if not indoor
        id = $(member).attr("ref");

        //Get height
        var height = $(xml).find('tag[k="height"]')[0];
        height = +$(height).attr('v');

        //Process the visual representation
        _processBuildingVisualInWay(src, $(src).find('#'+id)[0], convF, bulkLoader, 0, height, true, false, false, e);

        //Flag current id as processed
        _setAsProcessed(processed, id);
    }

    function _process3DModelVisual(jsonModel, textStatus, jqXHR) {
        //process transformation
        var params = jqXHR.userData;
        var owner = params.xml;
        function toNum(val) { return +val; }
        var translate = $($(owner).find('tag[k="model3DTranslate"]')[0]).attr('v').trim().split(/\s+/).map(toNum);
        var rot = $($(owner).find('tag[k="model3DRotation"]')[0]).attr('v').trim().split(/\s+/).map(toNum);
        var scale = $($(owner).find('tag[k="model3DScale"]')[0]).attr('v').trim().split(/\s+/).map(toNum);

        //Get node offset
        var offset = [];
        _processNode(owner, params.convF, offset);

        //Offset translate
        translate[0] += offset[0];
        translate[1] += params.yOffset ? params.yOffset : 0;
        translate[2] += offset[1];

        //Create model transformation matrix
        var modelTransform = mat4.identity();
        mat4.translate(modelTransform, translate, modelTransform);
        mat4.rotateX(modelTransform, rot[0], modelTransform);
        mat4.rotateY(modelTransform, rot[1], modelTransform);
        mat4.rotateZ(modelTransform, rot[2], modelTransform);
        mat4.scale(modelTransform, scale, modelTransform);

        //Apply transformation matrix to each vertex
        var pos = jsonModel.pos;
        for(var i = 0; i < pos.length; i+=3) {
            //Calculate
            var v = vec3.create([pos[i+0],pos[i+1],pos[i+2]]);
            mat4.multiplyVec3(modelTransform, v, v);

            //Save back to array
            pos[i+0] = v[0];
            pos[i+1] = v[1];
            pos[i+2] = v[2];
        }

        //Create normal matrix
        var normalTransform = mat4.toInverseMat3(modelTransform);
        mat3.transpose(normalTransform);
        normalTransform = mat3.toMat4(normalTransform);

        //Apply normal transform to each normal
        var nor = jsonModel.normal;
        for(var i = 0; i < nor.length; i+=3) {
            //Calculate
            var v = vec3.create([nor[i+0],nor[i+1],nor[i+2]]);
            mat4.multiplyVec3(normalTransform, v, v);

            //Save back to array
            nor[i+0] = v[0];
            nor[i+1] = v[1];
            nor[i+2] = v[2];
        }

        //Put result to entity
        params.entity.visual = {
            pos: jsonModel.pos,
            indices: jsonModel.indices,
            lineIndices: jsonModel.indices,
            normals: jsonModel.normal
        }

        //Done processing
        params.bulkLoader.finishLoader();
    }




    ////3D model

    function _getModel3D(xml) {
        return $($(xml).find('tag[k="model3D"]')[0]).attr('v');
    }

    function _loadModel3D(src, xml, convF, bulkLoader, yOffset, e) {
        //Increase loader counter
        bulkLoader.addLoader();

        //Do load the model
        var jqXHR = $.getJSON(
            $($(xml).find('tag[k="model3D"]')[0]).attr('v'), 
            _process3DModelVisual
        );
        jqXHR.userData = {   
            xml: xml, 
            entity: e, 
            convF: convF,
            yOffset: yOffset,
            bulkLoader: bulkLoader
        }
    }




    ////Indoor

    function _getFloorHeight(xml) {
        var tag = $(xml).find('tag[k="height"]')[0];
        return +$(tag).attr('v');
    }

    /**
     * Get outer visual representation of building,
     * which is shell in building level 0
     */
    function _processBuildingPartsInAFloor(src, xml, yOffset, convF, bulkLoader, processed, out) {
        var height = _getFloorHeight(xml) * _INDOOR_HEIGHT_MUL;

        $(xml).find('member').each(function() {
            if($(this).attr("role").toLowerCase() == "buildingpart") {
                var ref = $(this).attr("ref");
                ref = $(src).find("#" + ref)[0];

                //Create entity
                var e = {};
                _processOSMInfo(ref, e);
                _processBuildingVisualInWay(src, ref, convF, bulkLoader, yOffset, height, false, true, true, e);

                //Put it into entities
                out.push(e);
            }
        });
    }

    function _processBuildingParts(src, xml, convF, bulkLoader, processed, e) {
        //
        var ufloor = [];
        var bfloor = [];

        //Get each building part id
        $(xml).find("member").each(function() {
            var role = $(this).attr("role").toLowerCase();
            var ref  = $(this).attr("ref");
            if(role == "entrance") {
                //Do nothing, this is the node of enterance door
            }
            else {
                var level = +((role.split("_"))[1]);
                var floor = $(src).find("#"+ref)[0];
                if(level < 0) {
                    bfloor[Math.abs(level)-1] = ref;
                } else {
                    ufloor[level] = ref;
                }
            }
        });

        //Cumulative height
        var bfloorOffset = [];
        var ufloorOffset = [];
        var curOffset = 0;
        ufloorOffset = ufloor.map(function(val){
            var prevOffset = curOffset;
            curOffset += _getFloorHeight($(src).find('#'+val)[0]);
            return prevOffset;
        })
        curOffset = 0;
        bfloorOffset = bfloor.map(function(val){
            curOffset -= _getFloorHeight($(src).find('#'+val)[0]);
            return curOffset;
        })

        //Reorganize arrays
        var lowestLevel = bfloor.length > 0 ? -bfloor.length : 0;
        var floors = bfloor.reverse();
        floors = floors.concat(ufloor);

        var floorOffsets = bfloorOffset.reverse();
        floorOffsets = floorOffsets.concat(ufloorOffset);

        //Build data for building parts
        e.floors = [];
        floors.forEach(function(val, idx) {
            //Vars
            var xml = $(src).find("#" + val)[0];
            var buildingParts = [];
            var yOffset = floorOffsets[idx];

            //Process
            _processBuildingPartsInAFloor(src, xml, yOffset, convF, bulkLoader, processed, buildingParts);

            //Put to e
            e.floors.push(buildingParts);
        });
    }




    ////OSM Info

    function _processOSMInfo(xml, e) {
        //process
        var osmInfo = {
            id: $(xml).attr('id')
        };
        $(xml).find('tag').each(function(){
            var k = $(this).attr('k');
            var v = $(this).attr('v');

            osmInfo[k] = v;
        });

        //Put to entities
        e.osmInfo = osmInfo;
    }




    ////High level function for processing buildings

    function _processBuildingsWithIndoor(src, convF, processed, bulkLoader, entities) {
        $(src).find('relation').each(function(){
            if($(this).find('tag[k="building"]').length > 0) {
                //Make sure this is a valid indoor definition
                var member = $(this).find('member[role="level_0"]')[0];
                if(!member) return null; //Exit if not indoor

                //Create entity
                var e = {};

                //process components
                _processRelationVisual(src, this, processed, convF, bulkLoader, e);
                _processOSMInfo(this, e);
                _processBuildingParts(src, this, convF, bulkLoader, processed, e);

                //Put it into entities
                entities.push(e);
            }
        });
    }

    function _processBuildingsInWay(src, convF, processed, bulkLoader, entities) {
        $(src).find('way').each(function() {
            if($(this).find('tag[k="building"]').length > 0) {
                //Skip if this is already processed
                if(_hasProcessed(processed, _getID(this)))
                    return;

                //Create entity
                var e = {};
                _processOSMInfo(this, e);
                _processBuildingVisualInWay(src, this, convF, bulkLoader, 0, null, true, false, false, e);

                //Put it into entities
                entities.push(e);
            }
        });
    }




    ////Processed building management

    function _hasProcessed(processedIDs, id) {
        return processedIDs["#" + id];
    }

    function _setAsProcessed(processedIDs, id) {
        processedIDs["#" + id] = 1;
    }


    

    return {
        process: function(src, convF, doneCallback) {
            var processedIDs = [];
            var entities = [];
            var bulkLoader = BulkLoader.create().init(
                function(val) { doneCallback(entities); }
            );

            bulkLoader.addLoader();

            _processBuildingsWithIndoor(src, convF, processedIDs, bulkLoader, entities);
            _processBuildingsInWay(src, convF, processedIDs, bulkLoader, entities);
            //_processGround(src, convF, entities);

            bulkLoader.finishLoader();
        }
    };
});